/* CSc533 homework 4 - A Bumpy Mobile
 * Ian Ryan & Andrew Wilt 
 */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>

#include <GL/glew.h>
#include <GL/glu.h>
#include <GL/glut.h>

#include "raster.h"
#include "defs.h"
#include "hw4.h"
#include "mobile.h"
#include "bump.h"

int debug = 0;

static void display_CB ( void );
static void idle_CB ( void );
static void key_CB ( unsigned char key, int x, int y );
static void arrow_CB ( int key, int x, int y );
 
int width = WIDTH;
int height = HEIGHT;
int view0w = WIDTH;				// Mobile viewwindow width
int view0h = HEIGHT-PVIEWH;	// Mobile viewwindow height

static float camera [ 3 ] = { CAM_X, CAM_Y, CAM_Z }; 	// starting camera position
static float look [ 3 ] = { 0, 0, 1 }; 					// look vector from camera
static float look_angle = 45.;
static char assignment_name [] = "Hw4 - A Bumpy Mobile";

SG scene [MAX_PLATES];			// Array of scene plates
int max_depth = 1;             	// current maximum depth of the number of plates
int max_node = 0;				// Start with only the root node
int selected_plate = 0;			// First plate is selected
bool mobile_focus = true;		// Start with the focus on the mobile view
bool rotating = true;        	// Plates are rotating
bool use_shader = true;		// Don't use the shader

IMAGE image[ MAXIMAGE ]; 			// pointers to the two images to be read in
IMAGE orig_image[MAXIMAGE];			// images to be modified and used in the rendering
float THETA = 0;

int polyline [ MAX_POINTS ];
GLfloat normal [ SEGMENTS ][ 2 ];
int active_point = 0;
int recalc_normals = 0;

GLuint shaderProg [ 2 ];

void printLog (GLuint obj)
{
        int   infologLength = 0;
        GLint maxLength;

        if (glIsShader (obj))
                glGetShaderiv (obj, GL_INFO_LOG_LENGTH, &maxLength);
        else
                glGetProgramiv(obj, GL_INFO_LOG_LENGTH, &maxLength);

        char *infoLog = new char[maxLength];

        if (glIsShader(obj))
                glGetShaderInfoLog (obj, maxLength, &infologLength, infoLog);
        else
                glGetProgramInfoLog(obj, maxLength, &infologLength, infoLog);

        if (infologLength > 0)
                printf("%s\n",infoLog);
}

char* readShaderSource (const char* shaderFile)
{
        struct stat statBuf;
        FILE* fp = fopen(shaderFile, "r");
        char* buf;

        fstat (fileno(fp), &statBuf);
        buf = (char*) malloc ((statBuf.st_size + 1) * sizeof(char));
        memset (buf, '\0', statBuf.st_size + 1);

        fread (buf, 1, (size_t) statBuf.st_size, fp);
        fclose (fp);

        return buf;
}

void build_shader (GLuint shaderHandle, const char *vertShaderFile, const char *fragShaderFile)
{
        GLuint vertHandle;
        GLuint fragHandle;

        const GLchar *vertSource,
                                 *fragSource;

        /* load, compile, and attach the vertex shader */
        vertSource = readShaderSource (vertShaderFile);
        vertHandle = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource (vertHandle, 1, &vertSource, NULL);
        glCompileShader(vertHandle);
        glAttachShader (shaderHandle, vertHandle);

        fprintf (stdout, "Vertex Shader code:\n%s\n", vertSource );
        printLog(vertHandle);

        /* load, compile, and attach the fragment shader */
        fragSource = readShaderSource (fragShaderFile);
        fragHandle = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource (fragHandle, 1, &fragSource, NULL);
        glCompileShader(fragHandle);
        glAttachShader (shaderHandle, fragHandle);

        fprintf (stdout, "Fragment Shader code:\n%s\n", fragSource );
        printLog(fragHandle);
}

int main ( int argc, char *argv [] ) {

	int i, j;
	int win;

	GLenum glewErr;
	
	/* seed the random number generator */
	srand( time(NULL) );
	
	/* opengl setup */
	glutInit ( &argc, argv );
	glutInitDisplayMode ( GLUT_RGB | GLUT_DOUBLE );
	glutInitWindowSize ( width, height );
	win = glutCreateWindow ( assignment_name );
	glutDisplayFunc ( display_CB );
	glutIdleFunc ( idle_CB );
	glutKeyboardFunc ( key_CB );
	glutSpecialFunc ( arrow_CB );
	glClearColor ( ( GLclampf ) 0.0, ( GLclampf ) 0.0,
		( GLclampf ) 0.0, ( GLclampf ) 0.0 );
	gluOrtho2D ( 0, WIDTH, HEIGHT, 0 );

	/* clearing depth is everything less than 1 */
	glClearDepth( 1.0 );
	glEnable ( GL_DEPTH_TEST );
	glDepthFunc ( GL_LEQUAL );
	
	glEnable ( GL_CULL_FACE );
	glCullFace ( GL_BACK );
	
	glewErr = glewInit ();
	if ( glewErr != GLEW_OK ) {

		/* Problem: glewInit failed, something is seriously wrong. */
		fprintf(stderr, "Error: %s\n", glewGetErrorString (glewErr));
	}

	fprintf (stdout, "Status: Using GLEW %s\n", glewGetString(GLEW_VERSION));

	if (!GLEW_VERSION_2_0) {
		fprintf (stdout, "Error: Required extensions not supported.\n");
		fprintf (stdout, "  Program requires OpenGL version 2.0 or later.\n");
		exit   (-1);
	}

	/* read in both images */
	orig_image[0].img = read_jpeg_image ( FILE1 );
	orig_image[1].img = read_jpeg_image ( FILE2 );

	/* Set up  the initial scene of one plate */
	// bottom left hand corner of plate */
	scene[0].v[0].c[X] = (view0w - orig_image[0].img->width())/2;
	scene[0].v[0].c[Y] = (view0h - orig_image[0].img->height())/2;
	scene[0].v[0].c[Z] = 0;
	// top left hand corner of plate
	scene[0].v[1].c[X] = scene[0].v[0].c[X];
	scene[0].v[1].c[Y] = (view0h - scene[0].v[0].c[Y]);
	scene[0].v[1].c[Z] = 0;
	// top right hand corner of plate
	scene[0].v[2].c[X] = (view0w - scene[0].v[0].c[X]);
	scene[0].v[2].c[Y] = scene[0].v[1].c[Y];
	scene[0].v[2].c[Z] = 0;
	// bottom right hand corner of plate
	scene[0].v[3].c[X] = scene[0].v[2].c[X];
	scene[0].v[3].c[Y] = scene[0].v[0].c[Y];
	scene[0].v[3].c[Z] = 0;
	
	scene[0].rot = 0.0;
	scene[0].curr_angle = 0.0;
	
	/* initialize all polyline points to be 0 */
	for ( i = 0; i < MAX_POINTS; i++ ) {

		polyline [ i ] = 0;
	}

	/* initialize all normals */
	for ( i = 0; i < SEGMENTS; i++ ) {

		calculate_normals ( i );
	}
	
	/* generate textures */
	for(i=0; i<2; i++){
		/* modify the texture maps for this normal line */
		image[i].img = new ByteRaster(orig_image[i].img->width(), orig_image[i].img->height(), orig_image[i].img->channels());
		normalize_texture(orig_image[i].img, image[i].img);
	
		// loop through the different versions of the images and add them 
		// as textures 
		glGenTextures ( 1, &image[i].tex );
		glBindTexture ( GL_TEXTURE_2D, image[i].tex );
		glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
				GL_LINEAR_MIPMAP_LINEAR );
		gluBuild2DMipmaps ( GL_TEXTURE_2D, GL_RGB, 
				image[i].img->width(), image[i].img->height(),
				GL_RGB, GL_UNSIGNED_BYTE, image[i].img->head() );
        }

	/* shader stuff */
	shaderProg [ 0 ] = glCreateProgram ();
	shaderProg [ 1 ] = glCreateProgram ();

	build_shader ( shaderProg [ 0 ], "vertex1.glsl", "fragment1.glsl" );
	build_shader ( shaderProg [ 1 ], "vertex2.glsl", "fragment2.glsl" );

	glLinkProgram ( shaderProg [ 0 ] );
	glLinkProgram ( shaderProg [ 1 ] );

	glutMainLoop ();

	return EXIT_FAILURE;
}

static void display_CB ( void ) {
	int i, j, m, n;

	// Clear the buffer from previous rendering
	glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	/* Draw the mobile in the mobile view */
	glMatrixMode ( GL_PROJECTION );
	glLoadIdentity ();
	glViewport ( 0, PVIEWH, width, height );
	gluPerspective ( look_angle, 1, FRONT_CLIP, BACK_CLIP );
	glMatrixMode ( GL_MODELVIEW );
	glLoadIdentity ();

	gluLookAt ( camera [ X ], camera [ Y ], camera [ Z ], 
		camera [ X ] + look [ X ] * lens_distance, 
		camera [ Y ] + look [ Y ] * lens_distance,
		camera [ Z ] + look [ Z ] * lens_distance,
		0, 1, 0 );

	// Display each plate in the mobile
	draw_plate(ROOT, 0.0);

	if(use_shader)
		glUseProgram ( 0 );
	// If the mobile view has the focus, then highlight it
	if(mobile_focus){
		glColor3f(0, 1.0, 1.0);		// Cyan highlight
		glBegin(GL_LINE_LOOP);
			glVertex3f(0,-49,270);
			glVertex3f(0,height-151,270);
			glVertex3f(width,height-151, 270);
			glVertex3f(width,-49,270);
		glEnd();
	}
	
	// Set up a viewport for the poly line view
	glViewport ( 0, 0, width, PVIEWH );
	glMatrixMode ( GL_PROJECTION );
	glLoadIdentity();
	gluOrtho2D ( 0, width, 0, PVIEWH );

	if(use_shader)
		glUseProgram ( 0 );
	glMatrixMode ( GL_MODELVIEW );
	glLoadIdentity ();
	glColor3f(1.0, 1.0, 1.0);
	draw_polyline ();

	if ( debug )
		draw_normals ();	

	// if the polyline view has the focus, then highlight it.
	if(!mobile_focus){
		glColor3f(0, 1.0, 1.0);		// Cyan highlight
		glBegin(GL_LINE_LOOP);
			glVertex2i(5, 5);
			glVertex2i(width - 5, 5);
			glVertex2i(width - 5, PVIEWH - 5);
			glVertex2i(5, PVIEWH - 5);
		glEnd();
	}

	glutSwapBuffers ();

	return;
}

static void idle_CB ( void ) {

		glutPostRedisplay ();
}

static void key_CB ( unsigned char key, int x, int y ) {
	int old_max;
	int i;

	if(debug)
		printf ( "key - %c\n", key );

	switch ( key ) {

		case 'h':
			printf ( "option keys enabled:\n" );
			printf ( "h\tshow options\n" );
			printf ( "Arrow Keys\tSelect a mobile plate\n" );
			printf ( "+-\tAdd or Subtract a row of plates at the bottom\n" );

		case '+':
			/* Add a row of plates if not more than MAX_ROWS */
			if(mobile_focus){
				/* okay to add a row */
				if(max_node+1 < MAX_PLATES){
					add_row();
				}
			}
			break;

		case '-':
			/* Remove the bottom row of plates if > 1 */
			if(mobile_focus){
				// Only remove a row if not the root 
				if(max_node !=0)
					remove_row();
			}
			break;

		case 'r':
			/* Make the currently selected plate rotate faster in the Clockwise direction */
			if(mobile_focus){
				scene[selected_plate].rot += ROT_AMT;
			}
			break;

		case 'R':
			/* Make the currently selected plate rotate faster in the Counter-Clockwise direction */
			if(mobile_focus){
				scene[selected_plate].rot -= ROT_AMT;
			}
			break;

		case 'C':
			/* Make the currently selected plate rotate at some random speed */
			if(mobile_focus){
				int val = rand() % 75;
				scene[selected_plate].rot = (val) / 1000.0;
				if(debug)
					printf("random rotation amt: %f\n", scene[selected_plate].rot);
			}
			break;

		case 'P':
			/* Pause/Unpaus rotation for all plates */
			if(mobile_focus){
				rotating = !rotating;
			}
			break;
			
		case '\t':
			/* switch between mobile window focus and polyline window focus */
			mobile_focus = !mobile_focus;

			if ( debug ) {

				if ( mobile_focus )
					printf ( "now focusing on mobile viewport\n" );

				else
					printf ( "now focusing on polyline viewport\n" );
			}

			break;

		case 27:
			/* ESC key reset all points of polyline to original values */
			if(!mobile_focus){
			
				for ( i = 0; i < MAX_POINTS; i++ ) {

					polyline [ i ] = 0;
					calculate_normals ( i );
				}
			}

			break;
		case 's':
			/* toggle the fragment shader on and off */
			use_shader = !use_shader;
			if ( debug && use_shader)
				printf ( "shader programs turned on\n" );

			break;

		case 'd':
			/* toggle debugging on and off */
			debug = !debug;
			if ( debug )
				printf ( "debugging turned on\n" );

			break;

		case 'q':
			/* Quit the program */
			exit ( EXIT_SUCCESS );
			break;
	}

	if ( debug ) {
		printf ( "camera - { %f, %f, %f }\n", 
			camera [ X ], camera [ Y ], camera [ Z ] );
		printf ( "look - { %f, %f, %f }\n", 
			look [ X ], look [ Y ], look [ Z ] );
		printf ( "theta - %f\n", THETA );
	}

	glutPostRedisplay ();
}

void arrow_CB ( int key, int x, int y ) {

	switch ( key ) {

		    case GLUT_KEY_LEFT:
		    /* Make the plate to the left be the currently selected plate */
			if(mobile_focus){
				/* if selected is zero, then it stays zero */
				/* if in second row */
				if(selected_plate >= 1 && selected_plate <=2){
					selected_plate--;
					if(selected_plate < 1)
						selected_plate = 2;
				}
				/* if in third row */
				if(selected_plate >= 3 && selected_plate <= 6){
					selected_plate--;
					if(selected_plate < 3)
						selected_plate = 6;
				}
				/* if in fourth row */
				if(selected_plate >= 7 && selected_plate <= 14){
					selected_plate--;
					if(selected_plate < 7)
						selected_plate = 14;
				}
			} else {
				/* select the control point to the left of the current one */
				if ( active_point > 0 ) {
					active_point--;

					if ( debug ) {

						printf ( "active control point is %i\n", active_point );
					}
				}
			}

			break;

		case GLUT_KEY_RIGHT:
			/* Make the plate to the right be the currently selected plate */
			if(mobile_focus){
				/* if selected is zero, then it stays zero */
				/* if in second row */
				if(selected_plate >= 1 && selected_plate <=2){
					selected_plate ++;
					if(selected_plate > 2)
						selected_plate = 1;
				}
				/* if in third row */
				if(selected_plate >= 3 && selected_plate <= 6){
					selected_plate++;
					if(selected_plate > 6)
						selected_plate = 3;
				}
				/* if in fourth row */
				if(selected_plate >= 7 && selected_plate <= 14){
					selected_plate ++;
					if(selected_plate > 14)
						selected_plate = 7;
				}
			} else {
				/* select the control point to the right of the current one */
				if ( active_point + 1 < MAX_POINTS ) {
					active_point++;

					if ( debug ) {

						printf ( "active control point is %i\n", active_point );
					}
				}
			}

			break;

		case GLUT_KEY_UP:
			/* Make the parent plate be the currently selected plate */
			if(mobile_focus){
				if(((selected_plate-1)/2)>=0)
					selected_plate = (selected_plate-1)/2;
			} else {
				/* Move the current control point up */
				polyline [ active_point ]++;

				if ( debug )
					printf ( "p [ %i ] = %i\n", active_point, polyline [ active_point ] );
				recalc_normals = 1;
			}

			break;

		case GLUT_KEY_DOWN:
			/* Make the left child plate be the currently selected plate */
			if(mobile_focus){
				if(2*selected_plate+1 <= max_node)
					selected_plate = 2*selected_plate+1;
			} else {
				/* move the current control point down */
				polyline [ active_point ]--;

				if ( debug )
					printf ( "p [ %i ] = %i\n", active_point, polyline [ active_point ] );
				recalc_normals = 1;
			}

			break;

	}

	if ( recalc_normals ) {

		calculate_normals ( active_point );
		for(int i=0; i<MAXIMAGE; i++){
			normalize_texture(orig_image[i].img, image[i].img);
			gluBuild2DMipmaps ( GL_TEXTURE_2D, GL_RGB, 
				image[i].img->width(), image[i].img->height(),
				GL_RGB, GL_UNSIGNED_BYTE, image[i].img->head() );
		}
		recalc_normals = 0;
	}
}

/* find the unit vector for the segment between two points */
void find_unitVec(float* start, float* end, float* retval){
	float length;
	float dx, dy, slope;
	
	dx = end[X] - start[X];
	dy = end[Y] - start[Y];

	if ( dx == 0 ) {
		retval [X] = 1;
		retval [Y] = 0;
		retval [Z] = 0;
	}

	else {
		length = findDist(start, end);
		retval[X] = dx / length;
		retval[Y] = dy / length;
		retval[Z] = 0;
	}
}

/* find the distance from one point to another point */
float findDist(float* start, float* end){
	float dx, dy, dz;
	float length;
	
	dx = end[X]-start[X];
	dy = end[Y]-start[Y];
	dz = end[Z]-start[Z];

	return sqrt(dx*dx + dy*dy + dz*dz);
}
